@file:Suppress("RedundantVisibilityModifier")

package com.tiamosu.databinding.delegate

import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.annotation.MainThread
import androidx.annotation.RestrictTo
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.viewbinding.ViewBinding
import com.tiamosu.databinding.internal.checkMainThread
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

/**
 * @author tiamosu
 * @date 2021/8/28
 */
interface ViewBindingPropertyOrNull<in R : Any, out T : ViewBinding?> : ReadOnlyProperty<R, T> {

    @MainThread
    fun clear()
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open class LazyViewBindingPropertyOrNull<in R : Any, out T : ViewBinding>(
    private val onViewDestroyed: (T) -> Unit,
    protected val viewBinder: (R) -> T,
) : ViewBindingPropertyOrNull<R, T?> {

    private var viewBinding: T? = null

    @MainThread
    public override fun getValue(thisRef: R, property: KProperty<*>): T? {
        return viewBinding ?: viewBinder(thisRef).also { viewBinding ->
            this.viewBinding = viewBinding
        }
    }

    @MainThread
    @CallSuper
    public override fun clear() {
        if (viewBinding != null) {
            val viewBinding = viewBinding
            if (viewBinding != null) {
                onViewDestroyed.invoke(viewBinding)
            }
            (this.viewBinding as? ViewDataBinding)?.unbind()
            this.viewBinding = null
        }
    }
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open class EagerViewBindingPropertyOrNull<in R : Any, out T : ViewBinding>(
    private val viewBinding: T
) : ViewBindingPropertyOrNull<R, T?> {

    @MainThread
    public override fun getValue(thisRef: R, property: KProperty<*>): T? {
        return viewBinding
    }

    @MainThread
    public override fun clear() {
        // Do nothing
    }
}

@PublishedApi
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal class ViewGroupViewBindingPropertyOrNull<in V : ViewGroup, out T : ViewBinding>(
    onViewDestroyed: (T) -> Unit,
    viewBinder: (V) -> T
) : LifecycleViewBindingPropertyOrNull<V, T>(viewBinder, onViewDestroyed) {

    override fun getLifecycleOwner(thisRef: V): LifecycleOwner {
        return checkNotNull(thisRef.findViewTreeLifecycleOwner()) {
            "Fragment doesn't have view associated with it or the view has been destroyed"
        }
    }
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class LifecycleViewBindingPropertyOrNull<in R : Any, out T : ViewBinding>(
    private val viewBinder: (R) -> T,
    private val onViewDestroyed: (T) -> Unit,
) : ViewBindingPropertyOrNull<R, T?> {

    private var viewBinding: T? = null

    protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner

    @MainThread
    public override fun getValue(thisRef: R, property: KProperty<*>): T? {
        viewBinding?.let { return it }

        if (!isViewInitialized(thisRef)) {
            Log.e(TAG, ERROR_ACCESS_BEFORE_VIEW_READY)
            return null
        }

        if (ViewBindingPropertyDelegate.strictMode) {
            if (!runStrictModeChecks(thisRef)) {
                return null
            }
        }

        val lifecycle = getLifecycleOwner(thisRef).lifecycle
        return if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            this.viewBinding = null
            Log.e(TAG, ERROR_ACCESS_AFTER_DESTROY)
            // We can access to ViewBinding after Fragment.onDestroyView(),
            // but don't save it to prevent memory leak
            viewBinder.invoke(thisRef)
        } else {
            val viewBinding = viewBinder.invoke(thisRef)
            lifecycle.addObserver(ClearOnDestroyLifecycleObserver(this))
            this.viewBinding = viewBinding
            viewBinding
        }
    }

    private fun runStrictModeChecks(thisRef: R): Boolean {
        val lifecycle = getLifecycleOwner(thisRef).lifecycle
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            Log.e(TAG, ERROR_ACCESS_AFTER_DESTROY)
            return false
        }
        return true
    }

    /**
     * Check is host view ready to create viewBinding
     */
    protected open fun isViewInitialized(thisRef: R): Boolean {
        return true
    }

    @MainThread
    @CallSuper
    public override fun clear() {
        checkMainThread()
        val viewBinding = this.viewBinding
        this.viewBinding = null

        if (viewBinding != null) {
            onViewDestroyed.invoke(viewBinding)
            (viewBinding as? ViewDataBinding)?.unbind()
        }
    }

    internal fun postClear() {
        if (!mainHandler.post { clear() }) {
            clear()
        }
    }

    private class ClearOnDestroyLifecycleObserver(
        private val property: LifecycleViewBindingPropertyOrNull<*, *>
    ) : DefaultLifecycleObserver {

        @MainThread
        override fun onDestroy(owner: LifecycleOwner) {
            property.postClear()
        }
    }

    private companion object {

        private val mainHandler = Handler(Looper.getMainLooper())
    }
}

private const val TAG = "ViewBindingProperty"
private const val ERROR_ACCESS_BEFORE_VIEW_READY =
    "Host view isn't ready to create a ViewBinding instance"
private const val ERROR_ACCESS_AFTER_DESTROY =
    "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +
            "The instance of viewBinding will be not cached."
